/*
 *    Copyright 2022 The DSMS Authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.dsms.modules.alert;

import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.engine.TemplateFactory;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dsms.common.constant.*;
import com.dsms.common.prometheus.builder.QueryBuilderType;
import com.dsms.common.prometheus.builder.RangeQueryBuilder;
import com.dsms.common.prometheus.converter.result.DefaultQueryResult;
import com.dsms.common.prometheus.converter.result.MatrixData;
import com.dsms.common.taskmanager.service.ITaskService;
import com.dsms.common.util.PrometheusUtils;
import com.dsms.modules.alert.entity.AlertApprise;
import com.dsms.modules.alert.entity.AlertMessage;
import com.dsms.modules.alert.entity.AlertRule;
import com.dsms.modules.alert.service.IAlertAppriseService;
import com.dsms.modules.alert.service.IAlertMessageService;
import com.dsms.modules.alert.service.IAlertRuleService;
import com.dsms.modules.server.CommonUpdateServer;
import com.dsms.modules.util.PrometheusUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.net.URI;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;

@Slf4j
@Component
public class AlertJob {

    private static final String DEFAULT_STEP = "60s";
    private static final double CHECK_RATE = 0.9;

    private final IAlertRuleService alertRuleService;

    private final IAlertMessageService alertMessageService;

    private final IAlertAppriseService alertAppriseService;

    private final ITaskService taskService;

    public AlertJob(IAlertRuleService alertRuleService, IAlertMessageService alertMessageService, IAlertAppriseService alertAppriseService, ITaskService taskService) {
        this.alertRuleService = alertRuleService;
        this.alertMessageService = alertMessageService;
        this.alertAppriseService = alertAppriseService;
        this.taskService = taskService;
    }


    @Scheduled(cron = "0 * * * * ? ")
    public void alertRuleCheck() {
        log.info("Alert job execute ...");
        //if cluster not bind, skip task execute
        if (!taskService.validateBindCluster()) {
            return;
        }
        //1.get the alert rules
        List<AlertRule> validAlertRuleList = getValidAlertRuleList();
        if (ObjectUtils.isEmpty(validAlertRuleList)) {
            //Resend information that failed to be sent before
            List<AlertMessage> unSendMessages = alertMessageService.getUnSendMessages();
            alertAppriseService.notifyContact(unSendMessages);
            return;
        }

        //2.get alert message list
        List<AlertMessage> alertMessages = getAlertMessages(validAlertRuleList);
        if (ObjectUtils.isEmpty(alertMessages)) {
            return;
        }

        //3.storage alert message
        distinctSave(alertMessages);

        //4.notify contacts
        List<AlertMessage> unSendMessages = alertMessageService.getUnSendMessages();
        alertAppriseService.notifyContact(unSendMessages);

        //5.use ws push to front
        CommonUpdateServer.sendMessageToAll(CommonUpdateServer.ALERT_MESSAGE_CHANGED);

    }

    private List<AlertRule> getValidAlertRuleList() {
        List<AlertRule> alertRules = alertRuleService.list();

        alertRules.removeIf(alertRule -> {
            long minutes = ChronoUnit.MINUTES.between(alertRule.getLastCheckTime(), LocalDateTime.now());
            return minutes < alertRule.getRuleTimes();
        });
        if (ObjectUtils.isEmpty(alertRules)) {
            return Collections.emptyList();
        }
        alertRules.forEach(alertRule -> alertRule.setLastCheckTime(LocalDateTime.now()));
        alertRuleService.updateBatchById(alertRules);
        return alertRules;
    }


    /**
     * save the alert messages,but them should not be repeated
     */
    private void distinctSave(List<AlertMessage> alertMessages) {
        alertMessages.forEach(alertMessage -> {
            //get the same content alert messages order by updateTime
            LambdaQueryWrapper<AlertMessage> queryWrap = new LambdaQueryWrapper<AlertMessage>()
                    .eq(AlertMessage::getContent, alertMessage.getContent())
                    .orderByDesc(AlertMessage::getCreateTime);
            List<AlertMessage> selectedAlertMessages = alertMessageService.list(queryWrap);
            //if there have same alert message
            if (!selectedAlertMessages.isEmpty()) {
                //get the latest alert message
                AlertMessage latestAlertMessage = selectedAlertMessages.get(0);
                //if the status is unread,then update its times and updateTime instead of insert a new one
                if (Objects.equals(latestAlertMessage.getStatus(), AlertMessageStatusEnum.UN_CONFIRM.getStatus())) {
                    alertMessageService.updateById(
                            latestAlertMessage.setTimes(latestAlertMessage.getTimes() + 1)
                                    .setUpdateTime(alertMessage.getCreateTime())
                                    .setSmsStatus(alertMessage.getSmsStatus())
                                    .setEmailStatus(alertMessage.getEmailStatus())
                                    .setLevel(alertMessage.getLevel()));
                    return;
                }
            }
            alertMessageService.save(alertMessage);
        });
    }

    private List<AlertMessage> getAlertMessages(List<AlertRule> alertRules) {
        List<AlertMessage> alertMessageList = new ArrayList<>();

        for (AlertRule alertRule : alertRules) {
            //collect the dsms-storage original data
            DefaultQueryResult<MatrixData> ruleResult = collectRuleData(alertRule);
            //check whether the original data triggers the alert rules
            alertMessageList.addAll(checkRuleData(alertRule, ruleResult));
        }

        return alertMessageList;
    }

    /**
     * Collect the dsms-storage original data
     *
     * @param alertRule alertRule
     * @return original data
     */
    private DefaultQueryResult<MatrixData> collectRuleData(AlertRule alertRule) {
        RangeQueryBuilder rangeQueryBuilder = PrometheusUtil.getQueryBuilder(QueryBuilderType.RangeQuery);

        LocalDateTime endTime = LocalDateTime.now();
        LocalDateTime startTime = endTime.minusMinutes(alertRule.getRuleTimes());
        long start = startTime.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
        long end = endTime.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
        URI ruleUri = rangeQueryBuilder.build(alertRule.getRulePromql(), start, end, DEFAULT_STEP);
        return PrometheusUtils.convertQueryResultString(PrometheusUtil.generatePrometheusTemplate().getForObject(ruleUri, String.class));
    }

    private List<AlertMessage> checkRuleData(AlertRule alertRule, DefaultQueryResult<MatrixData> ruleResult) {
        List<AlertMessage> alertMessageList = new ArrayList<>();

        for (MatrixData matrixData : ruleResult.getResult()) {
            long valueCount = Arrays.stream(matrixData.getDataValues()).count();
            long notifyCount = Arrays.stream(matrixData.getDataValues()).filter(value -> isNotify(value.getValue(), alertRule)).count();
            if (notifyCount > (valueCount * CHECK_RATE)) {
                String content = renderTemplate(alertRule, matrixData.getMetric());
                alertMessageList.add(buildAlertMessage(alertRule, content));
            }
        }

        return alertMessageList;
    }

    private String renderTemplate(AlertRule alertRule, Map<String, String> params) {
        TemplateEngine templateEngine = TemplateFactory.get();
        //translate the following three special indicator thresholds to improve readability
        if (Objects.equals(alertRule.getRuleMetric(), AlertRuleTypeEnum.CEPH_HEALTH_WARN.getMetric())) {
            alertRule.setRuleThreshold(ClusterHealthStatusEnum.HEALTH_WARN.getStatus());
        } else if (Objects.equals(alertRule.getRuleMetric(), AlertRuleTypeEnum.CEPH_HEALTH_ERROR.getMetric())) {
            alertRule.setRuleThreshold(ClusterHealthStatusEnum.HEALTH_ERROR.getStatus());
        } else if (Objects.equals(alertRule.getRuleMetric(), AlertRuleTypeEnum.OSD_SLOW.getMetric())) {
            alertRule.setRuleThreshold(OsdHealthStatusEnum.SLOW_OPS.getStatus());
        }

        params.put(AlertRuleModuleEnum.THRESHOLD, alertRule.getRuleThreshold());
        params.put(AlertRuleModuleEnum.METRIC, AlertRuleTypeEnum.getalertRuleTypeEnum(alertRule.getRuleMetric()).getName());
        params.put(AlertRuleModuleEnum.COMPARE, CompareTypeEnum.getCompareType(alertRule.getRuleCompareType()).getDescription());
        params.put(AlertRuleModuleEnum.DEVICE, params.getOrDefault(AlertRuleModuleEnum.DEVICE, AlertRuleModuleEnum.DEVICE));
        params.put(AlertRuleModuleEnum.INSTANCE, params.getOrDefault(AlertRuleModuleEnum.INSTANCE, AlertRuleModuleEnum.INSTANCE));
        params.put(AlertRuleModuleEnum.RULE_NAME, params.getOrDefault(AlertRuleModuleEnum.RULE_NAME, AlertRuleModuleEnum.RULE_NAME));
        AlertRuleModuleEnum alertRuleModuleEnum = AlertRuleModuleEnum.getAlertRuleModuleEnum(alertRule.getRuleModule());
        return templateEngine.getTemplate(alertRuleModuleEnum.getModuleAlertTemplate()).render(params);
    }

    private AlertMessage buildAlertMessage(AlertRule alertRule, String content) {
        AlertMessage alertMessage = new AlertMessage();

        alertMessage.setRuleId(alertRule.getId());
        alertMessage.setModule(alertRule.getRuleModule());
        alertMessage.setLevel(alertRule.getRuleLevel());
        alertMessage.setContent(content);
        alertMessage.setCreateTime(LocalDateTime.now());
        alertMessage.setStatus(AlertMessageStatusEnum.UN_CONFIRM.getStatus());

        //get alertApprise status
        AlertMessageAppriseStatusEnum smsStatus = generateAlertMessageAppriseStatus(AlertAppriseTypeEnum.SMS);
        AlertMessageAppriseStatusEnum emailStatus = generateAlertMessageAppriseStatus(AlertAppriseTypeEnum.EMAIL);
        alertMessage.setSmsStatus(smsStatus.getCode());
        alertMessage.setEmailStatus(emailStatus.getCode());

        return alertMessage;
    }

    /**
     * generate the email/sms status according to alertApprise type and status
     *
     * @param appriseType
     * @return AlertMessageAppriseStatusEnum
     */
    private AlertMessageAppriseStatusEnum generateAlertMessageAppriseStatus(AlertAppriseTypeEnum appriseType) {
        List<AlertApprise> alertApprises = alertAppriseService.listNotifyAlertApprise();

        Optional<AlertApprise> result = alertApprises.stream().filter(alertApprise -> Objects.equals(alertApprise.getType(), appriseType.getCode())).findAny();

        if (result.isPresent()) {
            return AlertMessageAppriseStatusEnum.UP_BUT_NOT_APPRISED;
        } else {
            return AlertMessageAppriseStatusEnum.DOWN;
        }
    }

    /**
     * Check whether the monitoring metric reaches the threshold
     *
     * @param actualVal original value
     * @param alertRule alertRule
     * @return result
     */
    private boolean isNotify(Double actualVal, AlertRule alertRule) {
        String alertValue = alertRule.getRuleThreshold();
        if (actualVal == null || actualVal.isNaN() || actualVal.isInfinite()) {
            return false;
        }
        double doubleValue = Double.parseDouble(alertValue);
        int compareType = alertRule.getRuleCompareType();
        return compare(doubleValue, actualVal, compareType);
    }

    /**
     * Compare alertValue and actualValue with compareType
     * 0: EQUAL
     * 1: GREATER_THAN
     * -1: LESS_THAN
     * 2: NOT_EQUAL
     */
    private boolean compare(Double alertValue, Double actualValue, int compareType) {
        switch (CompareTypeEnum.getCompareType(compareType)) {
            case EQUAL:
                return alertValue.equals(actualValue);
            case GREATER_THAN:
                return actualValue.compareTo(alertValue) > 0;
            case LESS_THAN:
                return actualValue.compareTo(alertValue) < 0;
            case NOT_EQUAL:
                return !alertValue.equals(actualValue);
            default:
                return false;
        }

    }

}
